/*******************************************************************************
 * This file is part of blueprint4j www.blueprint4j.org.
 * 
 *     blueprint4j is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     blueprint4j is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with blueprint4j. If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package blueprint4j.comm;

import java.io.*;
import java.util.*;

import blueprint4j.utils.*;

public class XModem {

	public final static byte SOH = 0x01;
	public final static byte EOT = 0x04;
	public final static byte ACK = 0x06;
	public final static byte NAK = 0x15;
	public final static byte CAN = 0x18;

	private BufferedInputStream instream = null;
	private OutputStream outstream = null;

	public XModem(InputStream instream,OutputStream outstream) {
		this.instream = new BufferedInputStream(instream);
		this.outstream = outstream;
	}

	public byte[] read() throws IOException {
		try {
			VectorXPacket data = new VectorXPacket();
			long last_packet = 0;
			for (int i = 0;i < 600;i++) {
				try {
					XPacket xpacket = XPacket.read(instream);
					if (xpacket != null) {
						if (!data.add(xpacket)) {
							// ABPORT TRANSMISSION
							outstream.write(CAN);
							throw new IOException("Transmission aborted. Fatal Loss of Synchronization");
						} else {
							outstream.write(ACK);
							outstream.flush();
						}
						last_packet = System.currentTimeMillis();
						i = 0;
					}
					if (data.isDone()) {
						Thread.sleep(500);
						return data.toData();
					}
					if (System.currentTimeMillis() - last_packet > 10000) {
						outstream.write(NAK);
						outstream.flush();
					}
					Thread.sleep(100);
				} catch (XPacketError error) {
					Log.debug.out(error);
					error.printStackTrace();
					// RESYNC
					Thread.sleep(800);
					instream.skip(instream.available());
					outstream.write(NAK);
					outstream.flush();
				}
			}
			throw new XPacketError("Transmission timed out");
		} catch (InterruptedException ie) {
			throw new XPacketError(ie);
		}
	}

	public void write(byte [] p_data) throws IOException {
		try {
			VectorXPacket data = new VectorXPacket(p_data);
			Integer response = null;
			int cnt = 0;
			for (int i =0;i < 600;i++) {
				if (instream.available() > 0) {
					response = new Integer(instream.read());
				}
				if (response != null && response.intValue() == CAN) {
					throw new IOException("Transmission aborted. Fatal Loss of Synchronization");
				}
				if (response != null && response.intValue() == ACK) {
					if (++cnt == data.size()) {
						return;
					}

				}
				if (response != null) {
					data.get(cnt).write(outstream);
				}
				Thread.sleep(100);
			}
			throw new XPacketError("Transmission timed out");
		} catch (InterruptedException ie) {
			throw new XPacketError(ie);
		}
	}

	public static class XPacketError extends IOException {
		public XPacketError(String msg) {
			super(msg);
		}

		public XPacketError(Exception ex) {
			super(ex.getMessage());
			setStackTrace(ex.getStackTrace());
		}
	}

	private static class XPacket {
		public int header,block_num_255,checksum;
		public byte block_num;
		public byte [] data = new byte[128];
		private boolean last_packet = false;

		private XPacket(boolean last_packet) {
			this.last_packet =last_packet;
			data = new byte[]{EOT};
		}

		private XPacket(InputStream instream) throws IOException {
			header = instream.read();
			block_num = (byte)instream.read();
			block_num_255 = instream.read();
			instream.read(data);
			checksum = instream.read();
		}

		public XPacket(byte block_num,byte [] p_data) {
			this.block_num = block_num;
			if (p_data.length < 128) {
				data = new byte[128];
				for (int i =0;i < 128;i++) {
					if (i < p_data.length) {
						data[i] = p_data[i];
					} else {
						data[i] = 0;
					}
				}
			} else {
				this.data = p_data;
			}
			this.last_packet = last_packet;
			byte sum = 0;
			for (int i =0;i < data.length;i++) {
				sum += data[i];
			}
			checksum = sum;
		}

		public boolean valid() {
			byte sum = 0;
			for (int i =0;i < data.length;i++) {
				sum += data[i];
			}
			return (checksum == sum);
		}

		public boolean isLastPacket() {return last_packet;}

		public static XPacket read(InputStream instream) throws IOException {
			if (instream.available() == 0) {
				return null;
			}
			instream.mark(1);
			int soh = instream.read();
			instream.reset();
			if (soh == EOT) {
				return new XPacket(true);
			} else {
				if (soh != SOH) {
					throw new XPacketError("Incorrect Header ["+soh+"]");
				}
				if (instream.available() < 132) {
					return null;
				}
			}
			return new XPacket(instream);
		}

		public void write(OutputStream outstream) throws IOException {
			if (last_packet) {
				outstream.write(EOT);
			} else {
				outstream.write(SOH);
				outstream.write(block_num);
				outstream.write(block_num_255);
				outstream.write(data);
				outstream.write(checksum);
			}
			outstream.flush();
		}
	}

	private static class VectorXPacket {
		private Vector store=new Vector();

		public VectorXPacket() {}

		public VectorXPacket(byte [] data) {
			for (int i =0;i < (data.length / 128)+1;i++) {
				if (data.length < (i+1)*128) {
					add(new XPacket((byte)(i+1),ByteM.subbyte(data,i*128,data.length)));
				} else {
					add(new XPacket((byte)(i+1),ByteM.subbyte(data,i*128,(i+1)*128)));
				}
			}
			add(new XPacket(true));
		}

		public XPacket get(int pos){
			return (XPacket)store.get(pos);
		}

		public boolean add(XPacket item){
			if (item.last_packet) {
				store.add(item);
				return true;
			} else {
				if (size() > 0) {
					if (item.block_num == get(size()-1).block_num+1) {
						store.add(item);
						return true;
					} else if (item.block_num == get(size()-1).block_num) {
						// ALREADY ADDED
						return true;
					} else {
						return false;
					}
				} else {
					if (item.block_num == 1) {
						store.add(item);
						return true;
					} else {
						return false;
					}
				}
			}
		}

		public boolean remove(XPacket item){
			return store.remove(item);
		}

		public XPacket remove(int pos){
			return (XPacket) store.remove(pos);
		}

		public int size(){
			return store.size();
		}

		public boolean isDone() {
			if (size() > 0) {
				return get(size()-1).isLastPacket();
			} else {
				return false;
			}
		}

		public byte [] toData() {
			if (size() == 0) {
				return new byte[]{};
			}
			byte buffer[] = new byte[128*size()-1];
			for (int i =0;i < size()-1;i++) {
				for (int e = 0;e < 128;e++) {
					buffer[i*128+e] = get(i).data[e];
				}
			}
			return buffer;
		}
	}

	public static void main(String args[]) throws Exception {
		new Thread(
		   new Runnable() {
			   public void run() {
				   try {
					   java.net.ServerSocket ss = new java.net.ServerSocket(5000);
					   java.net.Socket s = ss.accept();
					   XModem modem = new XModem(s.getInputStream(),s.getOutputStream());
					   modem.write("Hello World This is a message that is longer than 128 bytes I Hope else it is a waste of time. The lastr pone was a waste of time but not this one. Nope this will be a sucess".getBytes());
					   s.close();
					   ss.close();
				   } catch (Exception ex) {
					   ex.printStackTrace();
				   }
			   }
		   }
		).start();
		new Thread(
		   new Runnable() {
			   public void run() {
				   try {
					   java.net.Socket s = new java.net.Socket("127.0.0.1",5000);
					   XModem modem = new XModem(s.getInputStream(),s.getOutputStream());
					   System.out.println("Read = " + new String(modem.read()));
					   s.close();
				   } catch (Exception ex) {
					   ex.printStackTrace();
				   }
			   }
		   }
		).start();
	}
}

